package mtvotingserver;

import java.util.*;
import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import javax.swing.*;

/**
 * Draw a plotter from multiple sets of sample points
 * 
 * @author Alex Hrin (0807133)
 */
public class Plotter extends JPanel {

    private ArrayList<DataTrack> dataTracks = new ArrayList<DataTrack>();
    private final int vPadding = 20;
    private final int hPadding = 125;
    private final int hPaddingRight = 20;
    private int maxPlotPointsPerTrack = 20;
    private String xLabel = "Abcissa";
    private String yLabel = "Ordinate";
    private Random rand = new Random();

    /**
     * Constructor
     * 
     * @param xLabel label for the X Axis
     * @param yLabel label for the Y Axis
     * @param maxPlotPointsPerTrack density of the plot - whenever this is 
     *                              surpassed by the number of sample points,
     *                              the plot will be resampled
     */
    public void init(String xLabel, String yLabel, int maxPlotPointsPerTrack) {
        this.xLabel = xLabel;
        this.yLabel = yLabel;
        this.maxPlotPointsPerTrack = maxPlotPointsPerTrack;
    }
    
    /**
     * Removes a set of sample points from the plot
     * @param name the name of the set
     */
    public void removeTrack(String name) {
        int index = 0;
        int target = -1;
        for (DataTrack dt : dataTracks) {
            if (dt.name.equals(name)) {
                target = index;
            }
            index++;
        }
        
        if (target >= 0)
            dataTracks.remove(target);
    }
    
    /**
     * Removes all the sample point sets from the plot
     */
    public void clearTracks() {
        dataTracks.clear();
    }
    
    /**
     * Clears all the sample points but keeps the tracks
     */
    public void clearValues() {
        for (DataTrack dt : dataTracks) {
            dt.reset();
        }
    }
    
    /**
     * Adds a new track and sample point set to the plot
     * @param name the name of the track
     * @param colour the colour of the track
     * @param xStart the x value of the first sample
     * @param yStart the y value of the first sample
     */
    public void addTrack(String name, Color colour, 
            double xStart, double yStart) {
        // If no colour is sent
        if (colour == null) {
            // Randomise the colour - keep values relatively discrete so the 
            // colours differences are meaningful enough
            int rChannel = rand.nextInt(7) * 32 + rand.nextInt(31);
            int gChannel = rand.nextInt(7) * 32 + rand.nextInt(31);
            int bChannel = rand.nextInt(7) * 32 + rand.nextInt(31);
            colour = new Color(rChannel, gChannel, bChannel);
        }
        
        dataTracks.add(new DataTrack(name, colour, xStart, yStart));
    }
    
    /**
     * Adds a new track and sample point set to the plot only if it doens't 
     * already exist
     * @param name the name of the track
     * @param colour the colour of the track
     * @param xStart the x value of the first sample
     * @param yStart the y value of the first sample
     */
    public void addTrackUnique(String name, Color colour, 
            double xStart, double yStart) {
        boolean found = false;
        for (DataTrack dt : dataTracks) {
            if (dt.name.equals(name)) {
                found = true;
                break;
            }
        }
        
        if (!found) {
            addTrack(name, colour, xStart, yStart);
        }
    }
    
    /**
     * Adds a new sample to a track
     * @param name the track name
     * @param xValue the sample's x value
     * @param yValue the sample's y value
     */
    public void pushToTrack(String name, double xValue, double yValue) {
        for (DataTrack dt : dataTracks) {
            if (dt.name.equals(name)) {
                dt.pushValues(xValue, yValue);
                if (dt.xAxisValues.size() > maxPlotPointsPerTrack)
                    dt.resample(maxPlotPointsPerTrack);
            }
        }
    }

    /**
     * Gets the maximum sample rate of the plot
     * @return the sample rate
     */
    public int getMaxPlotPointsPerTrack() {
        return maxPlotPointsPerTrack;
    }

    /**
     * Sets the maximum sample rate of the plot
     * @param maxPlotPointsPerTrack the sample rate
     */
    public void setMaxPlotPointsPerTrack(int maxPlotPointsPerTrack) {
        this.maxPlotPointsPerTrack = maxPlotPointsPerTrack;
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D)g;
        // Turn on antialiasing so the line slopes don't appear pixelated
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);
        // Get the host size since we're going to try to scale everything 
        // relative to it
        int w = getWidth();
        int h = getHeight();
        
        // Draw Y axis.
        g2.draw(new Line2D.Double(hPadding, vPadding, hPadding, h-vPadding));
        
        // Draw X axis.
        g2.draw(new Line2D.Double(hPadding, h-vPadding, 
                w-hPaddingRight, h-vPadding));
        
        
        // Draw labels.
        Font font = g2.getFont();
        FontRenderContext frc = g2.getFontRenderContext();
        LineMetrics lm = font.getLineMetrics("0", frc);
        float sh = lm.getAscent() + lm.getDescent();
        
        // Ordinate label
        float sy = vPadding + ((h - 2*vPadding) - 
                yLabel.length()*sh)/2 + lm.getAscent();
        for(int i = 0; i < yLabel.length(); i++) {
            String letter = String.valueOf(yLabel.charAt(i));
            float sw = (float)font.getStringBounds(letter, frc).getWidth();
            float sx = hPadding - hPaddingRight + (hPaddingRight - sw)/2;
            g2.drawString(letter, sx, sy);
            sy += sh;
        }
        
        // Abcissa label
        sy = h - vPadding + (vPadding - sh)/2 + lm.getAscent();
        float sw = (float)font.getStringBounds(xLabel, frc).getWidth();
        float sx = hPadding + (w - hPadding - sw)/2;
        g2.drawString(xLabel, sx, sy);
        
        // Track legend
        sy = vPadding;
        for (DataTrack dt : dataTracks) {
            lm = font.getLineMetrics(dt.name + "  ------", frc);
            sh = lm.getAscent() + lm.getDescent();
            sy += 1.5f * lm.getAscent();
            sw = (float)font.
                    getStringBounds(dt.name + "  ------", frc).getWidth();
            sx = hPaddingRight;
            g2.setColor(dt.colour);
            g2.drawString(dt.name + "  ------", sx, sy);
        }
        
        // Plot tracks
        double scaleX = (double)(w - hPadding - hPaddingRight)/getMaxX();
        double scaleY = (double)(h - 2*vPadding)/getMaxY();
        
        // Lines and points are drawn separately to avoid rendering artifacts
        // Draw lines.
        for (DataTrack dt : dataTracks) {
            
            
            g2.setPaint(dt.colour.darker());
            for(int i = 0; i < dt.xAxisValues.size() - 1; i++) {
                double x1 = hPadding + scaleX*dt.xAxisValues.get(i);
                double y1 = h - vPadding - scaleY*dt.yAxisValues.get(i);
                double x2 = hPadding + scaleX*dt.xAxisValues.get(i + 1);
                double y2 = h - vPadding - scaleY*dt.yAxisValues.get(i + 1);
                g2.draw(new Line2D.Double(x1, y1, x2, y2));
            }
        }
        
        // Mark data points.
        for (DataTrack dt : dataTracks) {
            
            g2.setPaint(dt.colour.brighter());
            for(int i = 0; i < dt.xAxisValues.size(); i++) {
                double x = hPadding + scaleX*dt.xAxisValues.get(i);
                double y = h - vPadding - scaleY*dt.yAxisValues.get(i);
                g2.fill(new Ellipse2D.Double(x-2, y-2, 4, 4));
            }
        }
    }
 
    /**
     * Gets the largest X value from all the sample point sets
     * @return the largest Y value on the plot
     */
    private double getMaxX() {
        double max = -Integer.MAX_VALUE;
        for (DataTrack dt : dataTracks) {
            double dtMaxX = dt.maxX();
            if(dtMaxX > max)
                max = dtMaxX;
        }
        return max;
    }
    
    /**
     * Gets the largest Y value from all the sample point sets
     * @return the largest Y value on the plot
     */
    private double getMaxY() {
        double max = -Integer.MAX_VALUE;
        for (DataTrack dt : dataTracks) {
            double dtMaxY = dt.maxY();
            if(dtMaxY > max)
                max = dtMaxY;
        }
        return max;
    }
    
    /**
     * Internal helper class to represent and manage a sample set
     */
    class DataTrack {
        public String name;
        public Color colour;
        public ArrayList<Double> xAxisValues = new ArrayList<Double>();
        public ArrayList<Double> yAxisValues = new ArrayList<Double>();
        public double tempX = 0.0;
        public double tempY = 0.0;

        /**
         * Constructor
         * @param name the name of the track
         * @param colour the colour of the track
         * @param xStart the x value of the first sample
         * @param yStart the y value of the first sample
         */
        public DataTrack(String name, Color colour, 
                double xStart, double yStart) {
            this.name = name;
            this.colour = colour;
            this.xAxisValues.add(xStart);
            this.yAxisValues.add(yStart);
        }
        
       /**
        * Resample the points on the track and reduces their count to a max of 
        * maxSize;
        * @param maxSize the maximum number of sample points in the set
        */
       public void resample(int maxSize) {
           int index = 0;
           ArrayList<Double> newXAxisValues = new ArrayList<Double>();
           ArrayList<Double> newYAxisValues = new ArrayList<Double>();
           int setValues = Math.max(xAxisValues.size() / maxSize, 2);
           
           // For each new sample point
           for (index = 0; index < xAxisValues.size() / setValues; index++) {
               // Find the effective value the sample point needs to take
               Double newXSample = xAxisValues.get(index * setValues);
               Double newYSample = yAxisValues.get(index * setValues);
               // Stop searching at the first valid value
               for (int cursor = setValues - 1; cursor > 0; cursor--) {
                   if (cursor < xAxisValues.size()) {
                       newXSample = xAxisValues.get(index * setValues + cursor);
                       newYSample = yAxisValues.get(index * setValues + cursor);
                       break;
                   }
               }
               // Add the new point to the track
               newXAxisValues.add(newXSample);
               newYAxisValues.add(newYSample);
           }
           
           // Replace the old sample points
           xAxisValues = newXAxisValues;
           yAxisValues = newYAxisValues;
       }
       
       /**
        * Adds a new point to the set
        * @param xValue the X value of the point
        * @param yValue the Y value of the point
        */
       public void pushValues(Double xValue, Double yValue) {
           int size = xAxisValues.size();
           // If the set already contains 2 points
           if ( size >= 2 ) {
               // Push to temp values initially
               tempX = xValue;
               tempY = yValue;
               // Only add if progress has been made (avoids adding duplicates)
               if ( xValue - xAxisValues.get(size - 1) >=  
                       xAxisValues.get(size - 1) - xAxisValues.get(size - 2)) {
                   xAxisValues.add(tempX);
                   yAxisValues.add(tempY); 
                   tempX = 0.0;
                   tempY = 0.0;
               }
           }
           // Else just add the point
           else {
               xAxisValues.add(xValue);
               yAxisValues.add(yValue); 
           }
           
           // Check if resampling is needed
           if (xAxisValues.size() >= maxPlotPointsPerTrack) {
               resample(maxPlotPointsPerTrack);
           }
       }
       
       /**
        * Gets the maximum X value of this set
        * @return the maximum X value
        */
       public double maxX() {
           double maxX = Double.MIN_VALUE;
           
           for (Double value : xAxisValues)
               if (value > maxX)
                   maxX = value;
           
           return maxX;
       }
       
       /**
        * Gets the maximum Y value of this set
        * @return the maximum Y value
        */
       public double maxY() {
           double maxY = Double.MIN_VALUE;
           
           for (Double value : yAxisValues)
               if (value > maxY)
                   maxY = value;
           
           return maxY;
       }

       /**
        * Resets the track by clearing the sample point arrays
        */
       private void reset() {
           yAxisValues.clear();
           xAxisValues.clear();
           tempX = 0.0;
           tempY = 0.0;
       }
    }
}
